﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Transactions;
using Flower.Directory.Util;
using System.Reflection;
using Flower.Services;
using Flower.Services.Data;
using Flower.Workflow;
using Assembly = System.Reflection.Assembly;
using AssemblyEntry = Flower.Services.Data.Assembly;
using Flower.Logging;

namespace Flower.Directory.Management
{
    public static class AssembliesImporter
    {
        private static readonly Log Log = LogManager.CreateLog(typeof(AssembliesImporter).Name);

        private const int ASSEMBLY_READ_BUFFER_SIZE = 1024 * 1024;

        public static void ImportAssemblies(this IDirectory directory, ICollection<string> paths)
        {
            Log.Info(string.Format("Assemblies to upload\n{0}", string.Join("\n", paths)));

            var dependencies = new List<AssemblyName>();

            ResolveEventHandler assemblyResolve =
                (sender, args) =>
                {
                    Log.Trace(() => string.Format("Resolving assembly {0}.", args.Name));

                    var assemblyNameToResolve = new AssemblyName(args.Name);

                    //Looking for the assembly in the paths.
                    foreach (string path in paths)
                    {
                        if (path.EndsWith(assemblyNameToResolve.Name + ".dll", StringComparison.Ordinal))
                        {
                            Log.Trace(string.Format("Assembly {0} found in the input. Loading from {1}.", args.Name, path));
                            return Assembly.ReflectionOnlyLoadFrom(path);
                        }
                    }

                    dependencies.Add(assemblyNameToResolve);

                    //Loading for the assembly in the current directory.
                    string assemblyFileName =
                        string.Concat(System.IO.Directory.GetCurrentDirectory(), @"\", assemblyNameToResolve.Name, ".dll");

                    if (System.IO.File.Exists(assemblyFileName))
                    {
                        Log.Trace(string.Format("Loading assembly {0} from the current directory {1}.", args.Name, System.IO.Directory.GetCurrentDirectory()));
                        return Assembly.ReflectionOnlyLoadFrom(assemblyFileName);
                    }
                    else
                    {
                        //Looking for the assembly in the base directory.
                        assemblyFileName =
                            string.Concat(AppDomain.CurrentDomain.BaseDirectory, assemblyNameToResolve.Name, ".dll");

                        if (System.IO.File.Exists(assemblyFileName))
                        {
                            Log.Trace(string.Format("Loading assembly {0} from the current domain base path {1}.", args.Name, AppDomain.CurrentDomain.BaseDirectory));
                            return Assembly.ReflectionOnlyLoadFrom(assemblyFileName);
                        }
                        else
                        {
                            Log.Trace(string.Format("Loading assembly {0} by name.", args.Name));
                            return Assembly.ReflectionOnlyLoad(assemblyNameToResolve.FullName);
                        }
                    }
                };

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += assemblyResolve;
                        
            try
            {
                foreach (string path in paths)
                {
                    Assembly asmToUpload = Assembly.ReflectionOnlyLoadFrom(path);
                    AssemblyName asmName = asmToUpload.GetName();

                    Log.Info(string.Format("Uploading assembly {0}.", asmToUpload.FullName));

                    DirectoryEntryInfo asmEntryInfo =
                        directory.GetInfo(DirectoryUtil.AssemblyNameToPath(asmName));

                    if (asmEntryInfo != null)
                    {
                        Log.Warning(string.Format("Assembly {0} is already uploaded. Rewriting.", asmToUpload.FullName));
                    }

                    string assemblyEntryParentPath = DirectoryUtil.MakeAssemblyEntryParentPath(asmName);

                    directory.UpdateWorkflowsOfAssembly(asmToUpload);

                    var assembly = new AssemblyEntry
                    {
                        Id = (asmEntryInfo != null) ? asmEntryInfo.Id : null,
                        Name = DirectoryUtil.MakeAssemblyEntryName(asmName),
                        AccessControlList = new List<AccessControlEntry> 
                        {
                            new AccessControlEntry 
                            {
                                Role = "Application",
                                Permission = Permission.Read
                            },
                            new AccessControlEntry 
                            {
                                Role = "Processor",
                                Permission = Permission.Read
                            }
                        }
                    };

                    using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, ASSEMBLY_READ_BUFFER_SIZE))
                    {
                        var buffer = new MemoryStream(ASSEMBLY_READ_BUFFER_SIZE);
                        stream.CopyTo(buffer);
                        assembly.Image = buffer.ToArray();
                    }

                    string asmId = directory.Save(assemblyEntryParentPath, assembly);

                    var latestLink = directory.Get(string.Concat(assemblyEntryParentPath, "/Latest")) as Link;
                    if (latestLink != null)
                    {
                        string latestAsmVersionPath = directory.GetPathById(latestLink.Target);
                        string latestAsmVersion;

                        DirectoryUtil.CutPathTailSegment(latestAsmVersionPath, out assemblyEntryParentPath, out latestAsmVersion);

                        if (asmName.Version.CompareTo(new Version(latestAsmVersion)) > 0)
                        {
                            latestLink.Target = asmId;

                            directory.Save(assemblyEntryParentPath, latestLink);
                        }
                    }
                    else
                    {
                        directory.Save
                        (
                            assemblyEntryParentPath, 
                            new Link 
                            {
                                Name = "Latest", 
                                Target = asmId,
                                AccessControlList = new List<AccessControlEntry> 
                                {
                                    new AccessControlEntry 
                                    {
                                        Role = "Application",
                                        Permission = Permission.Read
                                    },
                                    new AccessControlEntry 
                                    {
                                        Role = "Processor",
                                        Permission = Permission.Read
                                    }
                                }
                            }
                        );
                    }
                }
            }
            finally
            {
                AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= assemblyResolve;
            }

            var missingDependencies =
                dependencies
                    .Where
                    (
                        dep =>
                            !dep.Name.StartsWith("System", StringComparison.Ordinal) &&
                            !dep.Name.StartsWith("Flower", StringComparison.Ordinal) &&
                            !directory.Exists(DirectoryUtil.AssemblyNameToPath(dep), DateTime.MinValue)
                    )
                    .Select(dep => dep.FullName)
                    .ToList();

            if (missingDependencies.Count > 0)
            {
                Log.Warning
                (
                    string.Format
                    (
                        "The following dependencies are not found in the directory:\n{0}",
                        string.Join("\n", missingDependencies)
                    )
                );
            }
        }

        private static void UpdateWorkflowsOfAssembly(this IDirectory directory, Assembly assembly)
        {
            foreach (Type type in ExtractWorkflowDefinitionTypes(assembly))
            {
                Log.Info(string.Format("Workflow definition found {0}.", type.FullName));

                string workflowPath =
                    GetWorkflowPath(type) ??
                    string.Concat("/Workflows/", (type.FullName ?? type.Name).Replace('.', '/'));

                string workflowEntryName, workflowEntyParent;

                DirectoryUtil.CutPathTailSegment(workflowPath, out workflowEntyParent, out workflowEntryName);

                DirectoryEntry dirEntry = directory.Get(workflowPath);
                var workflowEntry = dirEntry as Services.Data.Workflow;

                if (dirEntry != null)
                {
                    if (workflowEntry != null)
                    {
                        var defAssemblyName = DirectoryUtil.AssemblyNameFromTypeName(workflowEntry.DefinitionType);

                        if (defAssemblyName != null)
                        {
                            if (defAssemblyName.Version != null)
                            {
                                if (defAssemblyName.Version.CompareTo(assembly.GetName().Version) < 0)
                                {
                                    Log.Info
                                    (
                                        string.Format
                                        (
                                            "Version of the assembly being uploaded " +
                                            "is greater than the assembly version of " +
                                            "the definition type {0} of the workflow {1}. " +
                                            "The workflow needs to be updated.",
                                            workflowEntry.DefinitionType,
                                            workflowPath
                                        )
                                    );

                                    workflowEntry.DefinitionType = type.AssemblyQualifiedName;

                                    directory.Save(workflowEntyParent, workflowEntry);
                                }
                                else
                                {
                                    Log.Info
                                    (
                                        string.Format
                                        (
                                            "The workflow {0} exists and doesn't need to be updated.",
                                            workflowPath
                                        )
                                    );
                                }
                            }
                            else
                            {
                                Log.Warning
                                (
                                    string.Format
                                    (
                                        "Failed to determine the assembly version of the definition type {0} of the workflow {1}. ",
                                        workflowEntry.DefinitionType,
                                        workflowPath
                                    )
                                );
                            }
                        }
                        else
                        {
                            Log.Warning
                            (
                                string.Format
                                (
                                    "The definition type {0} of the workflow {1} is not assembly qualified. " +
                                    "Unable to determine assembly version.",
                                    workflowEntry.DefinitionType,
                                    workflowPath
                                )
                            );
                        }
                    }
                    else
                    {
                        Log.Warning(string.Format("Directory entry {0} already exists and it is not a workflow.", workflowPath));
                    }
                }
                else
                {
                    Log.Info(string.Format("Creating workflow {0}.", workflowPath));

                    directory.Save
                    (
                        workflowEntyParent,
                        new Services.Data.Workflow
                        {
                            Name = workflowEntryName,
                            DefinitionType = type.AssemblyQualifiedName,
                            AccessControlList = 
                                GetWorkflowRoles(type)
                                    .Select
                                    (
                                        role => 
                                            new AccessControlEntry 
                                            {
                                                Role = role,
                                                Permission = Permission.Read
                                            }
                                    )
                                    .ToList()
                        }
                    );
                }
            }
        }

        private static IEnumerable<Type> ExtractWorkflowDefinitionTypes(Assembly assembly)
        {
            foreach (Type type in assembly.GetTypes())
            {
                bool isWorkflowDefinition =
                    type.FindInterfaces
                    (
                        (it, criteria) =>
                            it.IsGenericType && it.GetGenericTypeDefinition().FullName == typeof(IWorkflow<,>).FullName,
                        null
                    ).Length > 0;

                if (isWorkflowDefinition)
                {
                    yield return type;
                }
            }
        }

        private static string GetWorkflowPath(Type type)
        {
            foreach (CustomAttributeData attr in type.GetCustomAttributesData())
            {
                if
                (
                    attr.Constructor.DeclaringType != null &&
                    attr.Constructor.DeclaringType.FullName == typeof(PathAttribute).FullName
                )
                {
                    return
                        string.Concat
                        (
                            "/Workflows/",
                            attr.ConstructorArguments[0].Value.ToString(),
                            "/",
                            type.Name
                        );
                }
            }

            return null;
        }

        private static IEnumerable<string> GetWorkflowRoles(Type type)
        {
            bool rolesSpecified = false;

            foreach (CustomAttributeData attr in type.GetCustomAttributesData())
            {
                if
                (
                    attr.Constructor.DeclaringType != null &&
                    attr.Constructor.DeclaringType.FullName == typeof(AllowStartToAttribute).FullName
                )
                {
                    yield return attr.ConstructorArguments[0].Value.ToString();
                    
                    rolesSpecified = true;
                }
            }

            if (!rolesSpecified)
            {
                yield return "Application";
                yield return "Processor";
            }
        }
    }
}
